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.is_running = true;
59
60        self.transport
61            .take_message_receiver()
62            .ok_or_else(|| Error::Other("Message receiver already taken".to_string()))
63    }
64
65    /// Send a response back to the client for a given request.
66    pub async fn send_response(&self, event_id: &str, response: JsonRpcMessage) -> Result<()> {
67        self.transport.send_response(event_id, response).await
68    }
69
70    /// Publish server announcement.
71    pub async fn announce(&self) -> Result<nostr_sdk::EventId> {
72        self.transport.announce().await
73    }
74
75    /// Stop the gateway.
76    pub async fn stop(&mut self) -> Result<()> {
77        if !self.is_running {
78            return Ok(());
79        }
80        self.transport.close().await?;
81        self.is_running = false;
82        Ok(())
83    }
84
85    /// Check if the gateway is active.
86    pub fn is_active(&self) -> bool {
87        self.is_running
88    }
89}
90
91#[cfg(feature = "rmcp")]
92impl NostrMCPGateway {
93    /// Start a gateway directly from an rmcp server handler.
94    ///
95    /// This additive API keeps the existing `new/start/send_response` flow intact,
96    /// while also allowing direct `handler.serve(transport)` style usage.
97    pub async fn serve_handler<T, H>(
98        signer: T,
99        config: GatewayConfig,
100        handler: H,
101    ) -> Result<rmcp::service::RunningService<rmcp::RoleServer, H>>
102    where
103        T: nostr_sdk::prelude::IntoNostrSigner,
104        H: rmcp::ServerHandler,
105    {
106        use crate::NostrServerTransport;
107        use rmcp::ServiceExt;
108
109        let transport = NostrServerTransport::new(signer, config.nostr_config).await?;
110        handler
111            .serve(transport)
112            .await
113            .map_err(|e| Error::Other(format!("rmcp server initialization failed: {e}")))
114    }
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::core::types::*;
121    use crate::transport::server::NostrServerTransportConfig;
122    use std::time::Duration;
123
124    #[test]
125    fn test_gateway_config_construction() {
126        let nostr_config = NostrServerTransportConfig {
127            relay_urls: vec!["wss://relay.example.com".to_string()],
128            encryption_mode: EncryptionMode::Required,
129            gift_wrap_mode: GiftWrapMode::Optional,
130            server_info: Some(ServerInfo {
131                name: Some("Test Gateway".to_string()),
132                version: Some("1.0.0".to_string()),
133                ..Default::default()
134            }),
135            is_announced_server: true,
136            allowed_public_keys: vec!["abc123".to_string()],
137            excluded_capabilities: vec![],
138            max_sessions: 1000,
139            cleanup_interval: Duration::from_secs(120),
140            session_timeout: Duration::from_secs(600),
141            request_timeout: Duration::from_secs(60),
142        };
143
144        let config = GatewayConfig { nostr_config };
145
146        assert_eq!(
147            config.nostr_config.relay_urls,
148            vec!["wss://relay.example.com"]
149        );
150        assert_eq!(
151            config.nostr_config.encryption_mode,
152            EncryptionMode::Required
153        );
154        assert!(config.nostr_config.is_announced_server);
155        assert_eq!(config.nostr_config.allowed_public_keys.len(), 1);
156        assert!(
157            config
158                .nostr_config
159                .server_info
160                .as_ref()
161                .unwrap()
162                .name
163                .as_ref()
164                .unwrap()
165                == "Test Gateway"
166        );
167    }
168
169    #[test]
170    fn test_gateway_config_with_defaults() {
171        let config = GatewayConfig {
172            nostr_config: NostrServerTransportConfig::default(),
173        };
174        assert_eq!(
175            config.nostr_config.encryption_mode,
176            EncryptionMode::Optional
177        );
178        assert!(!config.nostr_config.is_announced_server);
179    }
180}