Skip to main content

contextvm_sdk/gateway/
mod.rs

1//! ContextVM Gateway — bridge a local MCP server to Nostr.
2//!
3//! The gateway receives MCP requests via Nostr and forwards them to a local
4//! MCP server, then publishes responses back to Nostr.
5
6use crate::core::error::{Error, Result};
7use crate::core::types::JsonRpcMessage;
8use crate::transport::server::{IncomingRequest, NostrServerTransport, NostrServerTransportConfig};
9
10/// Configuration for the gateway.
11#[derive(Debug, Clone)]
12#[non_exhaustive]
13pub struct GatewayConfig {
14    /// Nostr server transport configuration.
15    pub nostr_config: NostrServerTransportConfig,
16}
17
18impl GatewayConfig {
19    /// Create a new gateway configuration.
20    pub fn new(nostr_config: NostrServerTransportConfig) -> Self {
21        Self { nostr_config }
22    }
23}
24
25/// Gateway that bridges a local MCP server to Nostr.
26///
27/// The gateway listens for incoming MCP requests via Nostr, forwards them
28/// to a local MCP handler function, and sends responses back over Nostr.
29pub struct NostrMCPGateway {
30    transport: NostrServerTransport,
31    is_running: bool,
32}
33
34impl NostrMCPGateway {
35    /// Create a new gateway.
36    pub async fn new<T>(signer: T, config: GatewayConfig) -> Result<Self>
37    where
38        T: nostr_sdk::prelude::IntoNostrSigner,
39    {
40        let transport = NostrServerTransport::new(signer, config.nostr_config).await?;
41
42        Ok(Self {
43            transport,
44            is_running: false,
45        })
46    }
47
48    /// Start the gateway. Returns a receiver for incoming requests.
49    ///
50    /// The caller is responsible for processing requests and calling
51    /// `send_response` for each one.
52    pub async fn start(&mut self) -> Result<tokio::sync::mpsc::UnboundedReceiver<IncomingRequest>> {
53        if self.is_running {
54            return Err(Error::Other("Gateway already running".to_string()));
55        }
56
57        self.transport.start().await?;
58        self.transport.spawn_discoverability_publication();
59        self.is_running = true;
60
61        self.transport
62            .take_message_receiver()
63            .ok_or_else(|| Error::Other("Message receiver already taken".to_string()))
64    }
65
66    /// Send a response back to the client for a given request.
67    pub async fn send_response(&self, event_id: &str, response: JsonRpcMessage) -> Result<()> {
68        self.transport.send_response(event_id, response).await
69    }
70
71    /// Publish server announcement.
72    pub async fn announce(&self) -> Result<nostr_sdk::EventId> {
73        self.transport.announce().await
74    }
75
76    /// Stop the gateway.
77    pub async fn stop(&mut self) -> Result<()> {
78        if !self.is_running {
79            return Ok(());
80        }
81        self.transport.close().await?;
82        self.is_running = false;
83        Ok(())
84    }
85
86    /// Check if the gateway is active.
87    pub fn is_active(&self) -> bool {
88        self.is_running
89    }
90}
91
92#[cfg(feature = "rmcp")]
93impl NostrMCPGateway {
94    /// Start a gateway directly from an rmcp server handler.
95    ///
96    /// This additive API keeps the existing `new/start/send_response` flow intact,
97    /// while also allowing direct `handler.serve(transport)` style usage.
98    pub async fn serve_handler<T, H>(
99        signer: T,
100        config: GatewayConfig,
101        handler: H,
102    ) -> Result<rmcp::service::RunningService<rmcp::RoleServer, H>>
103    where
104        T: nostr_sdk::prelude::IntoNostrSigner,
105        H: rmcp::ServerHandler,
106    {
107        use crate::NostrServerTransport;
108        use rmcp::ServiceExt;
109
110        let transport = NostrServerTransport::new(signer, config.nostr_config).await?;
111        handler
112            .serve(transport)
113            .await
114            .map_err(|e| Error::Other(format!("rmcp server initialization failed: {e}")))
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::core::types::*;
122    use crate::transport::server::NostrServerTransportConfig;
123    use std::time::Duration;
124
125    #[test]
126    fn test_gateway_config_construction() {
127        let nostr_config = NostrServerTransportConfig {
128            relay_urls: vec!["wss://relay.example.com".to_string()],
129            encryption_mode: EncryptionMode::Required,
130            gift_wrap_mode: GiftWrapMode::Optional,
131            server_info: Some(ServerInfo {
132                name: Some("Test Gateway".to_string()),
133                version: Some("1.0.0".to_string()),
134                ..Default::default()
135            }),
136            is_announced_server: true,
137            allowed_public_keys: vec!["abc123".to_string()],
138            excluded_capabilities: vec![],
139            max_sessions: 1000,
140            cleanup_interval: Duration::from_secs(120),
141            session_timeout: Duration::from_secs(600),
142            request_timeout: Duration::from_secs(60),
143            relay_list_urls: None,
144            bootstrap_relay_urls: None,
145            publish_relay_list: true,
146            profile_metadata: None,
147            oversized_transfer: Default::default(),
148            open_stream: Default::default(),
149        };
150
151        let config = GatewayConfig { nostr_config };
152
153        assert_eq!(
154            config.nostr_config.relay_urls,
155            vec!["wss://relay.example.com"]
156        );
157        assert_eq!(
158            config.nostr_config.encryption_mode,
159            EncryptionMode::Required
160        );
161        assert!(config.nostr_config.is_announced_server);
162        assert_eq!(config.nostr_config.allowed_public_keys.len(), 1);
163        assert!(
164            config
165                .nostr_config
166                .server_info
167                .as_ref()
168                .unwrap()
169                .name
170                .as_ref()
171                .unwrap()
172                == "Test Gateway"
173        );
174    }
175
176    #[test]
177    fn test_gateway_config_with_defaults() {
178        let config = GatewayConfig {
179            nostr_config: NostrServerTransportConfig::default(),
180        };
181        assert_eq!(
182            config.nostr_config.encryption_mode,
183            EncryptionMode::Optional
184        );
185        assert!(!config.nostr_config.is_announced_server);
186    }
187}