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 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: ServerInfo = serde_json::from_str(&event.content).unwrap_or_default();
announcements.push(ServerAnnouncement {
pubkey: event.pubkey.to_hex(),
pubkey_parsed: event.pubkey,
server_info,
event_id: event.id,
created_at: event.created_at,
});
}
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)
}
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(),
};
assert_eq!(announcement.pubkey, pubkey.to_hex());
assert_eq!(announcement.server_info.name, Some("Test".to_string()));
}
}