Skip to main content

contextvm_sdk/discovery/
mod.rs

1//! Server discovery for the ContextVM protocol.
2//!
3//! Discover MCP servers and their capabilities (tools, resources, prompts)
4//! published as Nostr events on relays.
5//!
6//! # Example
7//!
8//! ```rust,no_run
9//! use contextvm_sdk::discovery;
10//! use contextvm_sdk::signer;
11//!
12//! # async fn example() -> contextvm_sdk::Result<()> {
13//! let keys = signer::generate();
14//! let relay_pool = contextvm_sdk::RelayPool::new(keys).await?;
15//! let relays = vec!["wss://relay.damus.io".to_string()];
16//! relay_pool.connect(&relays).await?;
17//! let client = relay_pool.client();
18//!
19//! let servers = discovery::discover_servers(client, &relays).await?;
20//! for server in &servers {
21//!     println!("Found server: {} ({:?})", server.pubkey, server.server_info.name);
22//!     let tools = discovery::discover_tools(client, &server.pubkey_parsed, &relays).await?;
23//!     println!("  Tools: {:?}", tools);
24//! }
25//! # Ok(())
26//! # }
27//! ```
28
29use 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/// A discovered server announcement.
39#[derive(Debug, Clone)]
40pub struct ServerAnnouncement {
41    /// Server public key (hex).
42    pub pubkey: String,
43    /// Parsed public key.
44    pub pubkey_parsed: PublicKey,
45    /// Server information from the announcement content.
46    pub server_info: ServerInfo,
47    /// The Nostr event ID of the announcement.
48    pub event_id: EventId,
49    /// When the announcement was created.
50    pub created_at: Timestamp,
51}
52
53/// Discover MCP servers by fetching kind 11316 announcement events from relays.
54pub async fn discover_servers(
55    client: &Arc<Client>,
56    _relay_urls: &[String],
57) -> Result<Vec<ServerAnnouncement>> {
58    let filter = Filter::new().kind(Kind::Custom(SERVER_ANNOUNCEMENT_KIND));
59
60    let events = client
61        .fetch_events(filter, Duration::from_secs(10))
62        .await
63        .map_err(|e| Error::Transport(e.to_string()))?;
64
65    let mut announcements = Vec::new();
66    for event in events {
67        let server_info: ServerInfo = serde_json::from_str(&event.content).unwrap_or_default();
68        announcements.push(ServerAnnouncement {
69            pubkey: event.pubkey.to_hex(),
70            pubkey_parsed: event.pubkey,
71            server_info,
72            event_id: event.id,
73            created_at: event.created_at,
74        });
75    }
76
77    Ok(announcements)
78}
79
80/// Discover tools published by a specific server (kind 11317).
81pub async fn discover_tools(
82    client: &Arc<Client>,
83    server_pubkey: &PublicKey,
84    _relay_urls: &[String],
85) -> Result<Vec<serde_json::Value>> {
86    fetch_list(client, server_pubkey, TOOLS_LIST_KIND, "tools").await
87}
88
89/// Discover resources published by a specific server (kind 11318).
90pub async fn discover_resources(
91    client: &Arc<Client>,
92    server_pubkey: &PublicKey,
93    _relay_urls: &[String],
94) -> Result<Vec<serde_json::Value>> {
95    fetch_list(client, server_pubkey, RESOURCES_LIST_KIND, "resources").await
96}
97
98/// Discover prompts published by a specific server (kind 11320).
99pub async fn discover_prompts(
100    client: &Arc<Client>,
101    server_pubkey: &PublicKey,
102    _relay_urls: &[String],
103) -> Result<Vec<serde_json::Value>> {
104    fetch_list(client, server_pubkey, PROMPTS_LIST_KIND, "prompts").await
105}
106
107/// Discover resource templates published by a specific server (kind 11319).
108pub async fn discover_resource_templates(
109    client: &Arc<Client>,
110    server_pubkey: &PublicKey,
111    _relay_urls: &[String],
112) -> Result<Vec<serde_json::Value>> {
113    fetch_list(
114        client,
115        server_pubkey,
116        RESOURCETEMPLATES_LIST_KIND,
117        "resourceTemplates",
118    )
119    .await
120}
121
122/// Discover tools and parse them into rmcp typed descriptors.
123#[cfg(feature = "rmcp")]
124pub async fn discover_tools_typed(
125    client: &Arc<Client>,
126    server_pubkey: &PublicKey,
127    relay_urls: &[String],
128) -> Result<Vec<rmcp::model::Tool>> {
129    let raw = discover_tools(client, server_pubkey, relay_urls).await?;
130    parse_typed_list(raw)
131}
132
133/// Discover resources and parse them into rmcp typed descriptors.
134#[cfg(feature = "rmcp")]
135pub async fn discover_resources_typed(
136    client: &Arc<Client>,
137    server_pubkey: &PublicKey,
138    relay_urls: &[String],
139) -> Result<Vec<rmcp::model::Resource>> {
140    let raw = discover_resources(client, server_pubkey, relay_urls).await?;
141    parse_typed_list(raw)
142}
143
144/// Discover prompts and parse them into rmcp typed descriptors.
145#[cfg(feature = "rmcp")]
146pub async fn discover_prompts_typed(
147    client: &Arc<Client>,
148    server_pubkey: &PublicKey,
149    relay_urls: &[String],
150) -> Result<Vec<rmcp::model::Prompt>> {
151    let raw = discover_prompts(client, server_pubkey, relay_urls).await?;
152    parse_typed_list(raw)
153}
154
155/// Discover resource templates and parse them into rmcp typed descriptors.
156#[cfg(feature = "rmcp")]
157pub async fn discover_resource_templates_typed(
158    client: &Arc<Client>,
159    server_pubkey: &PublicKey,
160    relay_urls: &[String],
161) -> Result<Vec<rmcp::model::ResourceTemplate>> {
162    let raw = discover_resource_templates(client, server_pubkey, relay_urls).await?;
163    parse_typed_list(raw)
164}
165
166// ── Internal ────────────────────────────────────────────────────────
167
168async fn fetch_list(
169    client: &Arc<Client>,
170    server_pubkey: &PublicKey,
171    kind: u16,
172    list_key: &str,
173) -> Result<Vec<serde_json::Value>> {
174    let filter = Filter::new()
175        .kind(Kind::Custom(kind))
176        .author(*server_pubkey);
177
178    let events = client
179        .fetch_events(filter, Duration::from_secs(10))
180        .await
181        .map_err(|e| Error::Transport(e.to_string()))?;
182
183    // Take the most recent event
184    let event = match events.into_iter().next() {
185        Some(e) => e,
186        None => return Ok(Vec::new()),
187    };
188
189    let parsed: serde_json::Value =
190        serde_json::from_str(&event.content).map_err(|e| Error::Other(e.to_string()))?;
191
192    Ok(parsed
193        .get(list_key)
194        .and_then(|v| v.as_array())
195        .cloned()
196        .unwrap_or_default())
197}
198
199#[cfg(feature = "rmcp")]
200fn parse_typed_list<T>(raw: Vec<serde_json::Value>) -> Result<Vec<T>>
201where
202    T: serde::de::DeserializeOwned,
203{
204    let mut parsed = Vec::new();
205    for item in raw {
206        let value = serde_json::from_value(item)
207            .map_err(|e| Error::Other(format!("Failed to parse typed discovery item: {e}")))?;
208        parsed.push(value);
209    }
210    Ok(parsed)
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use crate::core::types::ServerInfo;
217
218    #[test]
219    fn test_server_info_serialization() {
220        let info = ServerInfo {
221            name: Some("Test Server".to_string()),
222            version: Some("1.0.0".to_string()),
223            about: Some("A test MCP server".to_string()),
224            website: Some("https://example.com".to_string()),
225            picture: Some("https://example.com/pic.png".to_string()),
226        };
227
228        let json = serde_json::to_string(&info).unwrap();
229        let parsed: ServerInfo = serde_json::from_str(&json).unwrap();
230
231        assert_eq!(parsed.name, Some("Test Server".to_string()));
232        assert_eq!(parsed.version, Some("1.0.0".to_string()));
233        assert_eq!(parsed.about, Some("A test MCP server".to_string()));
234        assert_eq!(parsed.website, Some("https://example.com".to_string()));
235        assert_eq!(
236            parsed.picture,
237            Some("https://example.com/pic.png".to_string())
238        );
239    }
240
241    #[test]
242    fn test_server_info_default() {
243        let info = ServerInfo::default();
244        assert!(info.name.is_none());
245        assert!(info.version.is_none());
246        assert!(info.about.is_none());
247        assert!(info.website.is_none());
248        assert!(info.picture.is_none());
249    }
250
251    #[test]
252    fn test_server_info_partial_serialization() {
253        let info = ServerInfo {
254            name: Some("Minimal".to_string()),
255            ..Default::default()
256        };
257
258        let json = serde_json::to_string(&info).unwrap();
259        // Optional fields should be skipped
260        assert!(!json.contains("version"));
261        assert!(!json.contains("about"));
262        assert!(json.contains("Minimal"));
263    }
264
265    #[test]
266    fn test_server_info_deserialization_from_empty() {
267        let info: ServerInfo = serde_json::from_str("{}").unwrap();
268        assert!(info.name.is_none());
269    }
270
271    #[test]
272    fn test_server_announcement_struct() {
273        let keys = nostr_sdk::Keys::generate();
274        let pubkey = keys.public_key();
275
276        let announcement = ServerAnnouncement {
277            pubkey: pubkey.to_hex(),
278            pubkey_parsed: pubkey,
279            server_info: ServerInfo {
280                name: Some("Test".to_string()),
281                ..Default::default()
282            },
283            event_id: EventId::from_hex(
284                "0000000000000000000000000000000000000000000000000000000000000001",
285            )
286            .unwrap(),
287            created_at: Timestamp::now(),
288        };
289
290        assert_eq!(announcement.pubkey, pubkey.to_hex());
291        assert_eq!(announcement.server_info.name, Some("Test".to_string()));
292    }
293}