1use std::sync::Arc;
30use std::time::Duration;
31
32use nostr_sdk::prelude::*;
33
34use crate::core::constants::*;
35use crate::core::error::{Error, Result};
36use crate::core::types::ServerInfo;
37
38#[derive(Debug, Clone)]
40pub struct ServerAnnouncement {
41 pub pubkey: String,
43 pub pubkey_parsed: PublicKey,
45 pub server_info: ServerInfo,
47 pub event_id: EventId,
49 pub created_at: Timestamp,
51 pub protocol_version: Option<String>,
53 pub capabilities: Option<serde_json::Value>,
55 pub instructions: Option<String>,
57}
58
59pub async fn discover_servers(
61 client: &Arc<Client>,
62 _relay_urls: &[String],
63) -> Result<Vec<ServerAnnouncement>> {
64 let filter = Filter::new().kind(Kind::Custom(SERVER_ANNOUNCEMENT_KIND));
65
66 let events = client
67 .fetch_events(filter, Duration::from_secs(10))
68 .await
69 .map_err(|e| Error::Transport(e.to_string()))?;
70
71 let mut announcements = Vec::new();
72 for event in events {
73 let (server_info, protocol_version, capabilities, instructions) =
74 parse_announcement_content(&event.content);
75 announcements.push(ServerAnnouncement {
76 pubkey: event.pubkey.to_hex(),
77 pubkey_parsed: event.pubkey,
78 server_info,
79 event_id: event.id,
80 created_at: event.created_at,
81 protocol_version,
82 capabilities,
83 instructions,
84 });
85 }
86
87 Ok(announcements)
88}
89
90pub async fn discover_tools(
92 client: &Arc<Client>,
93 server_pubkey: &PublicKey,
94 _relay_urls: &[String],
95) -> Result<Vec<serde_json::Value>> {
96 fetch_list(client, server_pubkey, TOOLS_LIST_KIND, "tools").await
97}
98
99pub async fn discover_resources(
101 client: &Arc<Client>,
102 server_pubkey: &PublicKey,
103 _relay_urls: &[String],
104) -> Result<Vec<serde_json::Value>> {
105 fetch_list(client, server_pubkey, RESOURCES_LIST_KIND, "resources").await
106}
107
108pub async fn discover_prompts(
110 client: &Arc<Client>,
111 server_pubkey: &PublicKey,
112 _relay_urls: &[String],
113) -> Result<Vec<serde_json::Value>> {
114 fetch_list(client, server_pubkey, PROMPTS_LIST_KIND, "prompts").await
115}
116
117pub async fn discover_resource_templates(
119 client: &Arc<Client>,
120 server_pubkey: &PublicKey,
121 _relay_urls: &[String],
122) -> Result<Vec<serde_json::Value>> {
123 fetch_list(
124 client,
125 server_pubkey,
126 RESOURCETEMPLATES_LIST_KIND,
127 "resourceTemplates",
128 )
129 .await
130}
131
132#[cfg(feature = "rmcp")]
134pub async fn discover_tools_typed(
135 client: &Arc<Client>,
136 server_pubkey: &PublicKey,
137 relay_urls: &[String],
138) -> Result<Vec<rmcp::model::Tool>> {
139 let raw = discover_tools(client, server_pubkey, relay_urls).await?;
140 parse_typed_list(raw)
141}
142
143#[cfg(feature = "rmcp")]
145pub async fn discover_resources_typed(
146 client: &Arc<Client>,
147 server_pubkey: &PublicKey,
148 relay_urls: &[String],
149) -> Result<Vec<rmcp::model::Resource>> {
150 let raw = discover_resources(client, server_pubkey, relay_urls).await?;
151 parse_typed_list(raw)
152}
153
154#[cfg(feature = "rmcp")]
156pub async fn discover_prompts_typed(
157 client: &Arc<Client>,
158 server_pubkey: &PublicKey,
159 relay_urls: &[String],
160) -> Result<Vec<rmcp::model::Prompt>> {
161 let raw = discover_prompts(client, server_pubkey, relay_urls).await?;
162 parse_typed_list(raw)
163}
164
165#[cfg(feature = "rmcp")]
167pub async fn discover_resource_templates_typed(
168 client: &Arc<Client>,
169 server_pubkey: &PublicKey,
170 relay_urls: &[String],
171) -> Result<Vec<rmcp::model::ResourceTemplate>> {
172 let raw = discover_resource_templates(client, server_pubkey, relay_urls).await?;
173 parse_typed_list(raw)
174}
175
176fn parse_announcement_content(
186 content: &str,
187) -> (
188 ServerInfo,
189 Option<String>,
190 Option<serde_json::Value>,
191 Option<String>,
192) {
193 let Ok(value) = serde_json::from_str::<serde_json::Value>(content) else {
194 return (ServerInfo::default(), None, None, None);
195 };
196
197 if value.get("protocolVersion").is_some() {
199 let server_info = value
200 .get("serverInfo")
201 .map(server_info_from_implementation)
202 .unwrap_or_default();
203 let protocol_version = value
204 .get("protocolVersion")
205 .and_then(|v| v.as_str())
206 .map(String::from);
207 let capabilities = value.get("capabilities").cloned();
208 let instructions = value
209 .get("instructions")
210 .and_then(|v| v.as_str())
211 .map(String::from);
212 (server_info, protocol_version, capabilities, instructions)
213 } else {
214 let server_info = serde_json::from_value::<ServerInfo>(value).unwrap_or_default();
216 (server_info, None, None, None)
217 }
218}
219
220fn server_info_from_implementation(val: &serde_json::Value) -> ServerInfo {
226 ServerInfo {
227 name: val.get("name").and_then(|v| v.as_str()).map(String::from),
228 version: val
229 .get("version")
230 .and_then(|v| v.as_str())
231 .map(String::from),
232 website: val
233 .get("websiteUrl")
234 .and_then(|v| v.as_str())
235 .map(String::from),
236 about: val
237 .get("description")
238 .and_then(|v| v.as_str())
239 .map(String::from),
240 picture: None,
241 }
242}
243
244async fn fetch_list(
245 client: &Arc<Client>,
246 server_pubkey: &PublicKey,
247 kind: u16,
248 list_key: &str,
249) -> Result<Vec<serde_json::Value>> {
250 let filter = Filter::new()
251 .kind(Kind::Custom(kind))
252 .author(*server_pubkey);
253
254 let events = client
255 .fetch_events(filter, Duration::from_secs(10))
256 .await
257 .map_err(|e| Error::Transport(e.to_string()))?;
258
259 let event = match events.into_iter().next() {
261 Some(e) => e,
262 None => return Ok(Vec::new()),
263 };
264
265 let parsed: serde_json::Value =
266 serde_json::from_str(&event.content).map_err(|e| Error::Other(e.to_string()))?;
267
268 Ok(parsed
269 .get(list_key)
270 .and_then(|v| v.as_array())
271 .cloned()
272 .unwrap_or_default())
273}
274
275#[cfg(feature = "rmcp")]
276fn parse_typed_list<T>(raw: Vec<serde_json::Value>) -> Result<Vec<T>>
277where
278 T: serde::de::DeserializeOwned,
279{
280 let mut parsed = Vec::new();
281 for item in raw {
282 let value = serde_json::from_value(item)
283 .map_err(|e| Error::Other(format!("Failed to parse typed discovery item: {e}")))?;
284 parsed.push(value);
285 }
286 Ok(parsed)
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use crate::core::types::ServerInfo;
293
294 #[test]
295 fn test_server_info_serialization() {
296 let info = ServerInfo {
297 name: Some("Test Server".to_string()),
298 version: Some("1.0.0".to_string()),
299 about: Some("A test MCP server".to_string()),
300 website: Some("https://example.com".to_string()),
301 picture: Some("https://example.com/pic.png".to_string()),
302 };
303
304 let json = serde_json::to_string(&info).unwrap();
305 let parsed: ServerInfo = serde_json::from_str(&json).unwrap();
306
307 assert_eq!(parsed.name, Some("Test Server".to_string()));
308 assert_eq!(parsed.version, Some("1.0.0".to_string()));
309 assert_eq!(parsed.about, Some("A test MCP server".to_string()));
310 assert_eq!(parsed.website, Some("https://example.com".to_string()));
311 assert_eq!(
312 parsed.picture,
313 Some("https://example.com/pic.png".to_string())
314 );
315 }
316
317 #[test]
318 fn test_server_info_default() {
319 let info = ServerInfo::default();
320 assert!(info.name.is_none());
321 assert!(info.version.is_none());
322 assert!(info.about.is_none());
323 assert!(info.website.is_none());
324 assert!(info.picture.is_none());
325 }
326
327 #[test]
328 fn test_server_info_partial_serialization() {
329 let info = ServerInfo {
330 name: Some("Minimal".to_string()),
331 ..Default::default()
332 };
333
334 let json = serde_json::to_string(&info).unwrap();
335 assert!(!json.contains("version"));
337 assert!(!json.contains("about"));
338 assert!(json.contains("Minimal"));
339 }
340
341 #[test]
342 fn test_server_info_deserialization_from_empty() {
343 let info: ServerInfo = serde_json::from_str("{}").unwrap();
344 assert!(info.name.is_none());
345 }
346
347 #[test]
348 fn test_server_announcement_struct() {
349 let keys = nostr_sdk::Keys::generate();
350 let pubkey = keys.public_key();
351
352 let announcement = ServerAnnouncement {
353 pubkey: pubkey.to_hex(),
354 pubkey_parsed: pubkey,
355 server_info: ServerInfo {
356 name: Some("Test".to_string()),
357 ..Default::default()
358 },
359 event_id: EventId::from_hex(
360 "0000000000000000000000000000000000000000000000000000000000000001",
361 )
362 .unwrap(),
363 created_at: Timestamp::now(),
364 protocol_version: None,
365 capabilities: None,
366 instructions: None,
367 };
368
369 assert_eq!(announcement.pubkey, pubkey.to_hex());
370 assert_eq!(announcement.server_info.name, Some("Test".to_string()));
371 }
372
373 #[test]
374 fn test_parse_announcement_content_legacy_format() {
375 let content = r#"{"name":"Legacy Server","version":"0.1.0","about":"Old format"}"#;
376 let (info, pv, caps, instr) = super::parse_announcement_content(content);
377 assert_eq!(info.name.as_deref(), Some("Legacy Server"));
378 assert_eq!(info.version.as_deref(), Some("0.1.0"));
379 assert_eq!(info.about.as_deref(), Some("Old format"));
380 assert!(pv.is_none());
381 assert!(caps.is_none());
382 assert!(instr.is_none());
383 }
384
385 #[test]
386 fn test_parse_announcement_content_initialize_result_format() {
387 let content = r#"{
388 "protocolVersion": "2025-03-26",
389 "capabilities": {
390 "tools": { "listChanged": true },
391 "resources": { "subscribe": false, "listChanged": false }
392 },
393 "serverInfo": {
394 "name": "NewServer",
395 "version": "2.0.0",
396 "description": "Full InitializeResult",
397 "websiteUrl": "https://example.com"
398 },
399 "instructions": "Use tool X for Y"
400 }"#;
401 let (info, pv, caps, instr) = super::parse_announcement_content(content);
402
403 assert_eq!(info.name.as_deref(), Some("NewServer"));
404 assert_eq!(info.version.as_deref(), Some("2.0.0"));
405 assert_eq!(info.about.as_deref(), Some("Full InitializeResult"));
406 assert_eq!(info.website.as_deref(), Some("https://example.com"));
407 assert!(info.picture.is_none());
408
409 assert_eq!(pv.as_deref(), Some("2025-03-26"));
410 assert!(caps.is_some());
411 let caps = caps.unwrap();
412 assert!(caps.get("tools").is_some());
413 assert_eq!(instr.as_deref(), Some("Use tool X for Y"));
414 }
415
416 #[test]
417 fn test_parse_announcement_content_invalid_json() {
418 let (info, pv, caps, instr) = super::parse_announcement_content("not json");
419 assert!(info.name.is_none());
420 assert!(pv.is_none());
421 assert!(caps.is_none());
422 assert!(instr.is_none());
423 }
424
425 #[test]
426 fn test_parse_announcement_content_empty_object() {
427 let (info, pv, caps, instr) = super::parse_announcement_content("{}");
428 assert!(info.name.is_none());
429 assert!(pv.is_none());
430 assert!(caps.is_none());
431 assert!(instr.is_none());
432 }
433
434 #[test]
435 fn test_server_info_from_implementation() {
436 let val = serde_json::json!({
437 "name": "TestImpl",
438 "version": "3.0",
439 "title": "Fancy Title",
440 "description": "Impl description",
441 "websiteUrl": "https://impl.example.com"
442 });
443 let info = super::server_info_from_implementation(&val);
444 assert_eq!(info.name.as_deref(), Some("TestImpl"));
445 assert_eq!(info.version.as_deref(), Some("3.0"));
446 assert_eq!(info.website.as_deref(), Some("https://impl.example.com"));
447 assert_eq!(info.about.as_deref(), Some("Impl description"));
448 assert!(info.picture.is_none());
449 }
450}