use std::sync::Arc;
use std::time::Duration;
use nostr_sdk::prelude::*;
use crate::core::constants::*;
use crate::core::error::{Error, Result};
use crate::core::types::ServerInfo;
#[derive(Debug, Clone)]
pub struct ServerAnnouncement {
pub pubkey: String,
pub pubkey_parsed: PublicKey,
pub server_info: ServerInfo,
pub event_id: EventId,
pub created_at: Timestamp,
pub protocol_version: Option<String>,
pub capabilities: Option<serde_json::Value>,
pub instructions: Option<String>,
}
pub async fn discover_servers(
client: &Arc<Client>,
_relay_urls: &[String],
) -> Result<Vec<ServerAnnouncement>> {
let filter = Filter::new().kind(Kind::Custom(SERVER_ANNOUNCEMENT_KIND));
let events = client
.fetch_events(filter, Duration::from_secs(10))
.await
.map_err(|e| Error::Transport(e.to_string()))?;
let mut announcements = Vec::new();
for event in events {
let (server_info, protocol_version, capabilities, instructions) =
parse_announcement_content(&event.content);
announcements.push(ServerAnnouncement {
pubkey: event.pubkey.to_hex(),
pubkey_parsed: event.pubkey,
server_info,
event_id: event.id,
created_at: event.created_at,
protocol_version,
capabilities,
instructions,
});
}
Ok(announcements)
}
pub async fn discover_tools(
client: &Arc<Client>,
server_pubkey: &PublicKey,
_relay_urls: &[String],
) -> Result<Vec<serde_json::Value>> {
fetch_list(client, server_pubkey, TOOLS_LIST_KIND, "tools").await
}
pub async fn discover_resources(
client: &Arc<Client>,
server_pubkey: &PublicKey,
_relay_urls: &[String],
) -> Result<Vec<serde_json::Value>> {
fetch_list(client, server_pubkey, RESOURCES_LIST_KIND, "resources").await
}
pub async fn discover_prompts(
client: &Arc<Client>,
server_pubkey: &PublicKey,
_relay_urls: &[String],
) -> Result<Vec<serde_json::Value>> {
fetch_list(client, server_pubkey, PROMPTS_LIST_KIND, "prompts").await
}
pub async fn discover_resource_templates(
client: &Arc<Client>,
server_pubkey: &PublicKey,
_relay_urls: &[String],
) -> Result<Vec<serde_json::Value>> {
fetch_list(
client,
server_pubkey,
RESOURCETEMPLATES_LIST_KIND,
"resourceTemplates",
)
.await
}
#[cfg(feature = "rmcp")]
pub async fn discover_tools_typed(
client: &Arc<Client>,
server_pubkey: &PublicKey,
relay_urls: &[String],
) -> Result<Vec<rmcp::model::Tool>> {
let raw = discover_tools(client, server_pubkey, relay_urls).await?;
parse_typed_list(raw)
}
#[cfg(feature = "rmcp")]
pub async fn discover_resources_typed(
client: &Arc<Client>,
server_pubkey: &PublicKey,
relay_urls: &[String],
) -> Result<Vec<rmcp::model::Resource>> {
let raw = discover_resources(client, server_pubkey, relay_urls).await?;
parse_typed_list(raw)
}
#[cfg(feature = "rmcp")]
pub async fn discover_prompts_typed(
client: &Arc<Client>,
server_pubkey: &PublicKey,
relay_urls: &[String],
) -> Result<Vec<rmcp::model::Prompt>> {
let raw = discover_prompts(client, server_pubkey, relay_urls).await?;
parse_typed_list(raw)
}
#[cfg(feature = "rmcp")]
pub async fn discover_resource_templates_typed(
client: &Arc<Client>,
server_pubkey: &PublicKey,
relay_urls: &[String],
) -> Result<Vec<rmcp::model::ResourceTemplate>> {
let raw = discover_resource_templates(client, server_pubkey, relay_urls).await?;
parse_typed_list(raw)
}
fn parse_announcement_content(
content: &str,
) -> (
ServerInfo,
Option<String>,
Option<serde_json::Value>,
Option<String>,
) {
let Ok(value) = serde_json::from_str::<serde_json::Value>(content) else {
return (ServerInfo::default(), None, None, None);
};
if value.get("protocolVersion").is_some() {
let server_info = value
.get("serverInfo")
.map(server_info_from_implementation)
.unwrap_or_default();
let protocol_version = value
.get("protocolVersion")
.and_then(|v| v.as_str())
.map(String::from);
let capabilities = value.get("capabilities").cloned();
let instructions = value
.get("instructions")
.and_then(|v| v.as_str())
.map(String::from);
(server_info, protocol_version, capabilities, instructions)
} else {
let server_info = serde_json::from_value::<ServerInfo>(value).unwrap_or_default();
(server_info, None, None, None)
}
}
fn server_info_from_implementation(val: &serde_json::Value) -> ServerInfo {
ServerInfo {
name: val.get("name").and_then(|v| v.as_str()).map(String::from),
version: val
.get("version")
.and_then(|v| v.as_str())
.map(String::from),
website: val
.get("websiteUrl")
.and_then(|v| v.as_str())
.map(String::from),
about: val
.get("description")
.and_then(|v| v.as_str())
.map(String::from),
picture: None,
}
}
async fn fetch_list(
client: &Arc<Client>,
server_pubkey: &PublicKey,
kind: u16,
list_key: &str,
) -> Result<Vec<serde_json::Value>> {
let filter = Filter::new()
.kind(Kind::Custom(kind))
.author(*server_pubkey);
let events = client
.fetch_events(filter, Duration::from_secs(10))
.await
.map_err(|e| Error::Transport(e.to_string()))?;
let event = match events.into_iter().next() {
Some(e) => e,
None => return Ok(Vec::new()),
};
let parsed: serde_json::Value =
serde_json::from_str(&event.content).map_err(|e| Error::Other(e.to_string()))?;
Ok(parsed
.get(list_key)
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default())
}
#[cfg(feature = "rmcp")]
fn parse_typed_list<T>(raw: Vec<serde_json::Value>) -> Result<Vec<T>>
where
T: serde::de::DeserializeOwned,
{
let mut parsed = Vec::new();
for item in raw {
let value = serde_json::from_value(item)
.map_err(|e| Error::Other(format!("Failed to parse typed discovery item: {e}")))?;
parsed.push(value);
}
Ok(parsed)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::types::ServerInfo;
#[test]
fn test_server_info_serialization() {
let info = ServerInfo {
name: Some("Test Server".to_string()),
version: Some("1.0.0".to_string()),
about: Some("A test MCP server".to_string()),
website: Some("https://example.com".to_string()),
picture: Some("https://example.com/pic.png".to_string()),
};
let json = serde_json::to_string(&info).unwrap();
let parsed: ServerInfo = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.name, Some("Test Server".to_string()));
assert_eq!(parsed.version, Some("1.0.0".to_string()));
assert_eq!(parsed.about, Some("A test MCP server".to_string()));
assert_eq!(parsed.website, Some("https://example.com".to_string()));
assert_eq!(
parsed.picture,
Some("https://example.com/pic.png".to_string())
);
}
#[test]
fn test_server_info_default() {
let info = ServerInfo::default();
assert!(info.name.is_none());
assert!(info.version.is_none());
assert!(info.about.is_none());
assert!(info.website.is_none());
assert!(info.picture.is_none());
}
#[test]
fn test_server_info_partial_serialization() {
let info = ServerInfo {
name: Some("Minimal".to_string()),
..Default::default()
};
let json = serde_json::to_string(&info).unwrap();
assert!(!json.contains("version"));
assert!(!json.contains("about"));
assert!(json.contains("Minimal"));
}
#[test]
fn test_server_info_deserialization_from_empty() {
let info: ServerInfo = serde_json::from_str("{}").unwrap();
assert!(info.name.is_none());
}
#[test]
fn test_server_announcement_struct() {
let keys = nostr_sdk::Keys::generate();
let pubkey = keys.public_key();
let announcement = ServerAnnouncement {
pubkey: pubkey.to_hex(),
pubkey_parsed: pubkey,
server_info: ServerInfo {
name: Some("Test".to_string()),
..Default::default()
},
event_id: EventId::from_hex(
"0000000000000000000000000000000000000000000000000000000000000001",
)
.unwrap(),
created_at: Timestamp::now(),
protocol_version: None,
capabilities: None,
instructions: None,
};
assert_eq!(announcement.pubkey, pubkey.to_hex());
assert_eq!(announcement.server_info.name, Some("Test".to_string()));
}
#[test]
fn test_parse_announcement_content_legacy_format() {
let content = r#"{"name":"Legacy Server","version":"0.1.0","about":"Old format"}"#;
let (info, pv, caps, instr) = super::parse_announcement_content(content);
assert_eq!(info.name.as_deref(), Some("Legacy Server"));
assert_eq!(info.version.as_deref(), Some("0.1.0"));
assert_eq!(info.about.as_deref(), Some("Old format"));
assert!(pv.is_none());
assert!(caps.is_none());
assert!(instr.is_none());
}
#[test]
fn test_parse_announcement_content_initialize_result_format() {
let content = r#"{
"protocolVersion": "2025-03-26",
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": false, "listChanged": false }
},
"serverInfo": {
"name": "NewServer",
"version": "2.0.0",
"description": "Full InitializeResult",
"websiteUrl": "https://example.com"
},
"instructions": "Use tool X for Y"
}"#;
let (info, pv, caps, instr) = super::parse_announcement_content(content);
assert_eq!(info.name.as_deref(), Some("NewServer"));
assert_eq!(info.version.as_deref(), Some("2.0.0"));
assert_eq!(info.about.as_deref(), Some("Full InitializeResult"));
assert_eq!(info.website.as_deref(), Some("https://example.com"));
assert!(info.picture.is_none());
assert_eq!(pv.as_deref(), Some("2025-03-26"));
assert!(caps.is_some());
let caps = caps.unwrap();
assert!(caps.get("tools").is_some());
assert_eq!(instr.as_deref(), Some("Use tool X for Y"));
}
#[test]
fn test_parse_announcement_content_invalid_json() {
let (info, pv, caps, instr) = super::parse_announcement_content("not json");
assert!(info.name.is_none());
assert!(pv.is_none());
assert!(caps.is_none());
assert!(instr.is_none());
}
#[test]
fn test_parse_announcement_content_empty_object() {
let (info, pv, caps, instr) = super::parse_announcement_content("{}");
assert!(info.name.is_none());
assert!(pv.is_none());
assert!(caps.is_none());
assert!(instr.is_none());
}
#[test]
fn test_server_info_from_implementation() {
let val = serde_json::json!({
"name": "TestImpl",
"version": "3.0",
"title": "Fancy Title",
"description": "Impl description",
"websiteUrl": "https://impl.example.com"
});
let info = super::server_info_from_implementation(&val);
assert_eq!(info.name.as_deref(), Some("TestImpl"));
assert_eq!(info.version.as_deref(), Some("3.0"));
assert_eq!(info.website.as_deref(), Some("https://impl.example.com"));
assert_eq!(info.about.as_deref(), Some("Impl description"));
assert!(info.picture.is_none());
}
}